package math;

import java.util.*;
import java.util.stream.*;
import java.util.logging.*;
import math.CalculatorAssistant.Parser;
import math.CalculatorAssistant.Lexer;

public class Manager
{
    Parser parser;
    SyntaxNode ast;
    Scope globalScope;

    public Manager(Parser p)
    {
        this(p,new Scope());
    }

    public void setLexer(Lexer l)
    {
        parser.setLexer(l);
    }

    public Manager(Parser p,Scope s)
    {
        this.parser = p;
        this.globalScope = s;
    }

    public SyntaxNode parse()
    {
        SyntaxNode node = parser.parse();
        if(node instanceof Statement)
        {
            execute((Statement)node);
        }
        return node;
    }

    public void execute(Statement stat)
    {
        execute(stat,globalScope);
    }

    public void execute(Statement stat,Scope scope)
    {
        if(stat instanceof Assignment)
        {
            Assignment a = (Assignment)stat;
            VariableReferenceNode v = a.v;
            MathNode expression = a.expression;
            if(scope.contains(v.symbol))
            {
                Symbol va = scope.get(v.symbol);
                if(!(va instanceof Variable))
                {
                    throw new IllegalArgumentException(va + " is not a variable");
                }
                Variable variable = (Variable)va;
                variable.setValue(evaluate(expression,scope));
            }
            else
            {
                //throw new VariableUndefinedException(v.symbol + " is not defined");
                throw new IllegalArgumentException(v.symbol + " is not defined");
            }
        }
        else if(stat instanceof VariableDefinition)
        {
            VariableDefinition vd = (VariableDefinition)stat;
            String symbol = vd.symbol;
            MathNode value = vd.value;

            scope.register(symbol,new Variable(symbol,evaluate(value)));
        }
        else if(stat instanceof FunctionDefinition)
        {
            FunctionDefinition fd = (FunctionDefinition)stat;
            String symbol = fd.symbol;
            List<String> parameters = fd.parameters;
            List<MathNode> defaultValues = fd.defaultValues;
            MathNode body = fd.body;

            Function.Builder builder = Function.builder();
            scope.register(symbol,builder.symbol(symbol).optional(defaultValues.size()).necessary(parameters.size() - defaultValues.size()).defaultValue(defaultValues).parameters(parameters).body(body).build());
        }
        else
        {
            throw new IllegalArgumentException("Unknown statement: " + stat);
        }
    }

    public double evaluate(MathNode node)
    {
        return evaluate(node,globalScope);
    }

    public double evaluate(MathNode node,Scope scope)
    {
        if(node instanceof NumberNode)
        {
            return ((NumberNode)node).value;
        }
        else if(node instanceof LatexConstant)
        {
            Symbol s = scope.get(((LatexConstant)node).value);
            if(!(s instanceof Constant))
            {
                throw new IllegalArgumentException(s + " is not a constant");
            }
            Constant c = (Constant)s;
            return c.value;
        }
        else if(node instanceof UnaryOperatorNode)
        {
            UnaryOperatorNode uon = (UnaryOperatorNode)node;
            String operator = uon.operator;
            MathNode operand = uon.operand;

            switch(operator.charAt(0))
            {
                case'-':return 0 - evaluate(operand,scope);
                case'!':return (double)factorial((long)evaluate(operand,scope),operator.length());
            }
            throw new IllegalArgumentException("Occured " + getClass().getName() + " whose operator is: " + operator);
        }
        else if(node instanceof BinaryOperatorNode)
        {
            BinaryOperatorNode bon = (BinaryOperatorNode)node;
            String operator = bon.operator;
            MathNode leftOperand = bon.leftOperand;
            MathNode rightOperand = bon.rightOperand;

            double lValue = evaluate(leftOperand,scope);
            double rValue = evaluate(rightOperand,scope);
            switch(operator)
            {
                case"+": return lValue + rValue;
                case"-": return lValue - rValue;
                case"*": return lValue * rValue;
                case"/": return lValue / rValue;
                case"^": return Math.pow(lValue,rValue);
            }
            throw new IllegalArgumentException("Occured: " + getClass().getName() + " whose operator is: " + operator);
        }
        else if(node instanceof VariableReferenceNode)
        {
            VariableReferenceNode vrn = (VariableReferenceNode)node;
            String symbol = vrn.symbol;

            if(scope.contains(symbol))
            {
                Symbol s = scope.get(symbol);
                if(s instanceof Variable)
                {
                    Variable v = (Variable)s;
                    return v.value;
                }
                else if(s instanceof Constant)
                {
                    Constant c = (Constant)s;
                    return c.value;
                }
                else
                {
                    throw new IllegalArgumentException(symbol + " is not a variable");
                }
            }
            else
            {
                throw new IllegalArgumentException(symbol + " is not defined");
            }
        }
        else if(node instanceof FunctionCallNode)
        {
            FunctionCallNode fcn = (FunctionCallNode)node;
            String symbol = fcn.symbol;
            List<MathNode> arguments = fcn.arguments;

            if(!scope.contains(symbol))
            {
                throw new IllegalArgumentException(symbol + " is not a defined function");
            }

            if(!(scope.get(symbol) instanceof Function))
            {
                throw new IllegalArgumentException(symbol + " is not a function");
            }

            Function function = (Function)scope.get(symbol);
            int optionalArgumentCount = function.optionalArgumentCount;
            int necessaryArgumentCount = function.necessaryArgumentCount;
            List<MathNode> defaultValues = function.optionalArgumentDefaultValue;
            List<String> parameters = function.parameters;
            MathNode body = function.body;

            if(arguments.size() < necessaryArgumentCount)
            {
                throw new IllegalArgumentException("Not enough argument found.Calling " + symbol + " with " + arguments);
            }
            else if(arguments.size() > necessaryArgumentCount + optionalArgumentCount)
            {
                throw new IllegalArgumentException("Too much arguments calling " + symbol + ",arguments: " + arguments);
            }

            List<Double> evaluatedArguments = arguments.stream().map(mn -> Double.valueOf(evaluate(mn,scope))).collect(Collectors.toList());
            
            int actualArgumentCount = evaluatedArguments.size();
            List<Double> evaluatedOptionalArguments = defaultValues.stream().skip(actualArgumentCount - necessaryArgumentCount).map(mn -> Double.valueOf(evaluate(mn,scope))).collect(Collectors.toList());

            evaluatedArguments.addAll(evaluatedOptionalArguments);

            if(evaluatedArguments.size() != parameters.size())
            {
                Logger.getLogger("Manager").info("evaluatedArguments does not match parameters.evaluatedArguments: " + evaluatedArguments + ", parameters: " + parameters);
                System.exit(1);
            }

            Scope functionScope = new Scope(scope);

            for(int i = 0;i < parameters.size();i++)
            {
                functionScope.register(parameters.get(i),new Variable(parameters.get(i),evaluatedArguments.get(i)));
            }

            return evaluate(body,functionScope);
        }
        else if(node instanceof LatexFunction)
        {
            LatexFunction lf = (LatexFunction)node;
            String symbol = lf.symbol;
            List<MathNode> optionalArguments = lf.optionalArguments;
            List<MathNode> necessaryArguments = lf.necessaryArguments;

            int optionalCount = optionalArguments.size();
            int necessaryCount = necessaryArguments.size();
            checkArgumentCount(symbol,optionalCount,necessaryCount);

            switch(symbol)
            {
                case"frac":
                    return evaluate(necessaryArguments.get(0),scope) / evaluate(necessaryArguments.get(1),scope);
                case"sqrt":
                    return Math.pow(evaluate(necessaryArguments.get(0),scope),optionalArguments.isEmpty() ? (1.0 / 2.0) : (1.0 / evaluate(optionalArguments.get(0),scope)));
                case"sin":
                    return sin(evaluate(necessaryArguments.get(0),scope));
                case"cos":
                    return cos(evaluate(necessaryArguments.get(0),scope));
                case"tan":
                    return tan(evaluate(necessaryArguments.get(0),scope));
                case"csc":
                    return csc(evaluate(necessaryArguments.get(0),scope));
                case"sec":
                    return sec(evaluate(necessaryArguments.get(0),scope));
                case"cot":
                    return cot(evaluate(necessaryArguments.get(0),scope));
                case"arcsin":
                    return arcsin(evaluate(necessaryArguments.get(0),scope));
                case"arccos":
                    return arccos(evaluate(necessaryArguments.get(0),scope));
                case"arctan":
                    return arctan(evaluate(necessaryArguments.get(0),scope));
                case"ln":
                    return ln(evaluate(necessaryArguments.get(0),scope));
                case"log":
                    return log(optionalArguments.isEmpty() ? 10 : evaluate(optionalArguments.get(0),scope),evaluate(necessaryArguments.get(0),scope));
            }
            throw new IllegalArgumentException("I can't come up with no more exception message");
        }
        else if(node instanceof SpecialLatexFunction)
        {
            SpecialLatexFunction slf = (SpecialLatexFunction)node;
            String symbol = slf.symbol;
            Statement lowerBound = slf.lowerBound;
            MathNode upperBound = slf.upperBound;
            MathNode expression = slf.expression;
            double start = 0;
            double end = evaluate(upperBound,scope);

            String boundVariable = "";

            Scope latexScope = new Scope(scope);

            if(lowerBound instanceof FunctionDefinition)
            {
                throw new IllegalArgumentException("the lower bound cannot be a function definition");
            }
            else if(lowerBound instanceof Assignment)
            {
                Assignment a = (Assignment)lowerBound;
                VariableReferenceNode v = a.v;
                String varSymbol = v.symbol;
                start = evaluate(a.expression,scope);

                boundVariable = varSymbol;

                latexScope.register(varSymbol,new Variable(varSymbol,start));
            }
            else if(lowerBound instanceof VariableDefinition)
            {
                VariableDefinition vd = (VariableDefinition)lowerBound;

                start = evaluate(vd.value,scope);

                boundVariable = vd.symbol;

                execute(lowerBound,latexScope);
            }
            else
            {
                throw new IllegalArgumentException("Unknown statement in latex function call");
            }

            if(boundVariable == "")
            {
                Logger.getLogger("Manager").info("boundVariable is not assigned");
                System.exit(1);//never happens
            }

            switch(symbol)
            {
                case"sum":return sum(start,end,expression,latexScope,boundVariable);
                case"prod":return prod(start,end,expression,latexScope,boundVariable);
                case"int":return integrate(start,end,expression,latexScope,boundVariable);
            }

            throw new IllegalArgumentException("Unknown latex function");
        }
        else
        {
            throw new IllegalArgumentException("haha");
        }
    }

    private static final Map<String,List<Integer>> LATEX_FUNCTION_ARGUMENT_COUNTS = new HashMap<>(){{
            put("frac",List.of(0,2));
            put("sqrt",List.of(1,1));
            put("sin",List.of(0,1));
            put("cos",List.of(0,1));
            put("tan",List.of(0,1));
            put("sec",List.of(0,1));
            put("csc",List.of(0,1));
            put("cot",List.of(0,1));
            put("arcsin",List.of(0,1));
            put("arccos",List.of(0,1));
            put("arctan",List.of(0,1));
            put("log",List.of(1,1));
            put("ln",List.of(0,1));
    }};
            

    private void checkArgumentCount(String function,int oc,int nc)
    {
        List<Integer> counts = LATEX_FUNCTION_ARGUMENT_COUNTS.get(function);
        if(oc > counts.get(0) || nc != counts.get(1))
        {
            throw new IllegalArgumentException(function + " called with not matched arguments on quantity");
        }
    }

    private Variable getBoundVariable(String bv,Scope scope)
    {
        Symbol s = scope.get(bv);
        if(!(s instanceof Variable))
        {
            throw new IllegalArgumentException(bv + " is not a variable");
        }
        return (Variable)s;
    }

    private double sum(double start,double end,MathNode expression,Scope scope,String boundVariable)
    {
        double result = 0;
        Variable v = getBoundVariable(boundVariable,scope);

        for(int i = (int)start;i <= (int)end;i++)
        {
            v.setValue(i);
            result += evaluate(expression,scope);
        }

        return result;
    }

    private double prod(double start,double end,MathNode expression,Scope scope,String boundVariable)
    {
        double result = 1;
        Variable v = getBoundVariable(boundVariable,scope);

        for(int i = (int)start;i <= (int)end;i++)
        {
            v.setValue(i);
            result *= evaluate(expression,scope);
        }

        return result;
    }

    private double integrate(double start,double end,MathNode expression,Scope scope,String boundVariable)
    {
        int time = (1 << 20);
        double step = (end - start) / time;
        System.err.println("time: " + time + ", step: " + step);
        Variable v = getBoundVariable(boundVariable,scope);

        double result = 0;
        for(int i = 1;i <= time;i++)
        {   
            v.setValue(start + step * i);
            result += evaluate(expression,scope) * step;
        }

        return result;
    }
        
    private long factorial(long number,int step)
    {
        if(number < 0)
        {
            throw new IllegalArgumentException("The factorialized expression cannot be negative.Found: " + number + "!");
        }
        if(number == 0 || number == 1)
        {
            return 1;
        }

        long result = 1;
        for(long i = number;i > 0;i -= step)
        {
            result *= i;
        }
        return result;
    }

    private double aroundResult(double r)
    {
        if(Math.abs(r) < 1e-10)
        {
            return 0.0;
        }
        if(Math.abs(r - 1.0) < 1e-10)
        {
            return 1.0;
        }
        if(Math.abs(r + 1.0) < 1e-10)
        {
            return -1.0;
        }
        return r;
    }

    private double sin(double number)
    {
        double result = Math.sin(number);
        return aroundResult(result);
    }

    private double cos(double number)
    {
        double result = Math.cos(number);
        return aroundResult(result);
    }

    private double tan(double number)
    {
        double result = Math.tan(number);
        return aroundResult(result);
    }

    private double cot(double number)
    {
        return 1.0 / tan(number);
    }

    private double csc(double number)
    {
        return 1.0 / sin(number);
    }

    private double sec(double number)
    {
        return 1.0 / cos(number);
    }

    private double arcsin(double number)
    {
        return Math.asin(number);
    }

    private double arccos(double number)
    {
        return Math.acos(number);
    }

    private double arctan(double number)
    {
        return Math.atan(number);
    }

    private double log(double x,double y)
    {
        return Math.log10(y) / Math.log10(x);
    }

    private double ln(double number)
    {
        return log(10,number) / log(10,Math.E);
    }

}
